跳到主要内容

Gin 实现热更新

注意,以下的热更新是使用 Linux 的信号量实现的,使用 Windows 这篇就跳过吧(没救了,等死吧)

Linux 中的信号

内核在某些情况下发送信号,比如在进程往一个已经关闭的管道写数据时会产生 SIGPIPE 信号,在终端执行特定的组合键可以使系统发送特定的信号给此进程,完成一系列的动作

命令        信号        含义
ctrl + c SIGINT 强制进程结束
ctrl + z SIGTSTP 任务中断,进程挂起
ctrl + \ SIGQUIT 进程结束 和 dump core
ctrl + d EOF
SIGHUP 终止收到该信号的进程。若程序中没有捕捉该信号,当收到该信号时,进程就会退出(常用于 重启、重新加载进程)

因此在我们执行 ctrl + c 关闭 gin 服务端时,会强制进程结束,导致正在访问的用户等出现问题

常见的 kill -9 pid 会发送 SIGKILL 信号给进程,也是类似的结果,注意,尽量少用 kill -9,因为会导致程序直接进行关闭,没有给进程留下一点回旋的余地,这样很糟糕,程序需要进行资源的回收,所以可能会导致一些问题。

完整的信号量类型如下:

# -l 可以打印出支持的信号量
kill -l

1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

Gin 的热更新

目的

  • 不关闭现有连接(正在运行中的程序)
  • 新的进程启动并替代旧进程
  • 新的进程接管新的连接
  • 连接要随时响应用户的请求,当用户仍在请求旧进程时要保持连接,新用户应请求新进程,不可以出现拒绝请求的情况

流程

1、替换可执行文件或修改配置文件 2、发送信号量 SIGHUP 3、拒绝新连接请求旧进程,但要保证已有连接正常 4、启动新的子进程 5、新的子进程开始 Accet 6、系统将新的请求转交新的子进程 7、旧进程处理完所有旧连接后正常结束

借助 fvbock/endless 来实现 Golang HTTP/HTTPS 服务重新启动的零停机

go get -u github.com/fvbock/endless

endless server 监听以下几种信号量:

  • syscall.SIGHUP:触发 fork 子进程和重新启动
  • syscall.SIGUSR1/syscall.SIGTSTP:被监听,但不会触发任何动作
  • syscall.SIGUSR2:触发 hammerTime
  • syscall.SIGINT/syscall.SIGTERM:触发服务器关闭(会完成正在运行的请求)

endless 正正是依靠监听这些信号量,完成管控的一系列动作

把原本的 main 函数

func main() {
router := routers.InitRouter()

s := &http.Server{
Addr: fmt.Sprintf(":%d", setting.HTTPPort),
Handler: router,
ReadTimeout: setting.ReadTimeout,
WriteTimeout: setting.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}

err := s.ListenAndServe()
if err != nil {
panic(err)
}
}

改成如下:

func main() {
endless.DefaultReadTimeOut = setting.ReadTimeout
endless.DefaultWriteTimeOut = setting.WriteTimeout
endless.DefaultMaxHeaderBytes = 1 << 20
endPoint := fmt.Sprintf(":%d", setting.HTTPPort)

server := endless.NewServer(endPoint, routers.InitRouter())
server.BeforeBegin = func(add string) {
log.Printf("Actual pid is %d", syscall.Getpid())
}

err := server.ListenAndServe()
if err != nil {
log.Printf("Server err: %v", err)
}
}

启动成功后,输出了 pid 为 17346;

在另外一个终端执行 kill -1 17346

可以看到该命令已经挂起,并且 fork 了新的子进程 pid 为 17689

17346 Received SIGTERM.
17346 Waiting for connections to finish...
17346 Serve() returning...
Server err: accept tcp [::]:9999: use of closed network connection
17346 [::]:9999 Listener closed.

大致意思为主进程(pid为 17346)接受到 SIGTERM 信号量,关闭主进程的监听并且等待正在执行的请求完成;

这时候在 postman 上再次访问我们的接口,你可以惊喜的发现,他“复活”了!

但是 endless 热更新是采取创建子进程后,将原进程退出的方式,这点不符合守护进程的要求

Reference

「连载七」优雅的重启服务